<?php
/*
	simpleurlmapper.php

	A simple PHP-Based URL Mapper using the simple form of URI Templates.

	Copyright (c) 2008, Mike Schinkel
	All rights reserved.

	Redistribution and use in source and binary forms, with or without modification, are
	permitted provided that the following conditions are met:

		 Redistributions of source code must retain the above copyright notice, this list of
			conditions and the following disclaimer.

		 Redistributions in binary form must reproduce the above copyright notice, this list
			of conditions and the following disclaimer in the documentation and/or other materials
			provided with the distribution.

		 Neither the name of the SimpleUrlMapper Team nor the names of its contributors may be
			used to endorse or promote products derived from this software without specific prior
			written permission.

	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
	OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
	AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
	SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
	OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
	POSSIBILITY OF SUCH DAMAGE.

	@package SimpleUrlMapper
	@version 0.96 beta
	@copyright 2008 Mike Schinkel
	@author Mike Schinkel
	@link http://code.google.com/p/simpleurlmapper/ SimpleUrlMapper
	@license http://www.opensource.org/licenses/bsd-license.php New BSD License

	UPDATE NOTES:
		-- Changed internal terminology from function => action, validate => validator.
		-- Implemented class and instance methods in addition to functions for actions and validators.
		-- Change datatypes to gettype()/settype() values.

	AFTER CODE REVIEW AT ATLANTA PHP:
		-- Changed from class functions to instance functions
		-- Used "self::" in a few places
		-- Also, unrelated to code review, changed names from variable() to add_variable() and  template()
			 to add_template().  For add_variable() and add_template() gave different signatures from variable()
			 and template() => Used ($name,$info) where $info provided everything needed besides name.

*/
class SimpleUrlMapper {
	const VERSION = 'v0.96 beta';
	public static $debugging= true;
	private $templates;
	private $variables;

	/*
		SimpleUrlMapper()

		Constructor initializes the lists of templates and variables.

	*/
	public function SimpleUrlMapper() {
		$this->templates = array();
		$this->variables = array();
	}
	/*
		template_count()

		Returns the number of named templates that have been added.

	*/
	public function template_count() {
		return count($this->templates);
	}
	/*
		variable_count()

		Returns the number of named template variables that have been added.

	*/
	public function variable_count() {
		return count($this->variables);
	}
	/*
		add_variable($name,$description,$validator,$type)

		Adds a new variable to be parsed from URLs by SimpleUrlMapper.

			$name =>
				Name of the variable, i.e. "post_id" or "image_id"

			$info =>
				Info about the variable. Could include a 'description', 'validator' or 'type' (or more later?)

				'description' =>
					A text description of this variable.  To be used in error handling but also for simple code
					documentation.

				'validator' =>
					A value that identifies the validation for the URL variable.
					A string defines a callback function that itself expects an assoicative array of URL variable/values
					An array of two values defines an inclusive range.
					More options for validate to be defined as this code evolves

				'type' =>
					A type string that identifies the data type expected for the URL variable.
					Provides some level of validation.

	*/
	public function add_variable($name,$info) {
		extract($info);
		$description = (isset($description) ? $description : $name);
		$type =	(isset($type) ? $type : 'integer');
		if (!isset($validator) || (!is_array($validator) && !function_exists($validator))) {
			header('HTTP/1.1 500 Internal Server Error');
			if (is_string($validator)) {
				print 'ERROR in template() => The "validator" parameter is a function name but ' . "[$validator()] does not exist.";
			} else {
				print 'ERROR in template() => The "validator" parameter is not an array nor a function name.';
			}
			exit();
		}
		$this->variables[$name] = array(
			'name' => $name,
			'description' => $description,
			'validator' => $validator,
			'type' => $type,
		);
	}
	/*
		add_template($name,$template,$action)

		Adds a new named URI Template to be matched.

			$name =>
				Name of template given to it so it can be referenced in get_path()

			$info =>
				Info about the template, including 'template' and 'action'

				'template' =>
					String containing simple URI template (i.e. does not support advanced URI Template syntax).

				'action' =>
					Function name or Array with Class & Method Name to execute on matching template.

		NOTES:
			-- Requires any template variables to be predefined to allow for error checking.
			-- Generates a HTTP 500 status code if variable in template or functions not found.

	*/
	public function add_template($name,$info) {
		extract($info);
		if (!isset($template)) {
			header('HTTP/1.1 500 Internal Server Error');
			print "ERROR in template() => Template not provided.";
			exit();
		}
		if (!isset($action)) {
			header('HTTP/1.1 500 Internal Server Error');
			print "ERROR in template() => Action not provided.";
			exit();
		}
		$template = trim($template,'/');
		if (preg_match_all('#{([^}]+)}#',$template,$matches)) {
			$variables = $matches[1];
			$literals = preg_split('#{[^}]+}#',$template);
			$regex = '#' . implode('([^/]+)',$literals) . '#';
		} else {
			$regex = '#' . $template . '#';
		}
		foreach($variables as $key => $variable) {
			$variables[$key] = $variable = strtolower($variable);
			if (!isset($this->variables[$variable])) {
				header('HTTP/1.1 500 Internal Server Error');
				print "ERROR in template() => Variable [$variable] was not defined.";
				exit();
			}
		}
		if (!function_exists($action) &&
				!(is_array($action) && count($action)==2 && method_exists($action[0],$action[1]))
		) {
			header('HTTP/1.1 500 Internal Server Error');
			print "ERROR in template() => Function [$action()] does not exist.";
			exit();
		}
		$this->templates[$name] = array(
			'name' => 		$name,
			'template' => $template,
			'action' => $action,
			'variables' =>$variables,
			'regex' => 		$regex,
			'literals' => $literals,
		);
	}
	/*
		execute($options=array())

		Call associated function for the template that first matches the path.

			$options => Optional, String or Array
				If String => $path
				If Array => elements may be $path, $pass_as_array, $must_match

					$path => Optional [default=null]
						The path to match.
						Path can have leading and trailing slashes, or not but should NOT contain the domain.
						If null or not passed, defaults to $_SERVER['REQUEST_URI'].

					$pass_as_array => Optional [default=true]
						Determines parameter passing behavior for function called when URL path matches

						-- If true, passes values as a single associative array, i.e.
								$action = 'my_path_func';
								$data= array(
									'foo' => 1,
									'bar' => 2,
									'baz' => 3,
								);
								call_user_func($action, $data);
								function my_path_func($data) {
									// do something
								}

						-- If false, passes values as regular parameters.
								$action = 'my_path_func';
								$data= array(
									'foo' => 1,
									'bar' => 2,
									'baz' => 3,
								);
								call_user_func_array($action`, $data);
								function my_path_func($foo, $bar, $baz) {
									// do something
								}

					$must_match =>  Optional [default=true]
						Determines parameter passing behavior for function called when URL path matches

		NOTES:
			-- Generates a HTTP 404 status code if $path not matched unless #must_match=false.
	*/
	public function execute($options = array()) {
		if (is_string($options)) {
			$options= array('path' => $options);
		}
		$path = 				(isset($options['path']) ? $options['path'] : $_SERVER['REQUEST_URI']);
		$pass_as_array= (isset($options['pass_as_array']) ? $options['pass_as_array'] : true);
		$must_match= 		(isset($options['must_match']) ? $options['must_match'] : true);

		$path = trim($path,'/');
		list($path) = explode('?',$path);
		$matched = false;
		foreach($this->templates as $template) {
			if (preg_match($template['regex'],$path,$matches)) {
				$data = $this->get_data($template,$matches);
				if ($pass_as_array) {
					call_user_func($template['action'],$data);
				} else {
					call_user_func_array($template['action'],$data);
				}
				exit();
			}
		}
		if ($must_match) {
			$this->show_no_match();
		}
	}

	/*
		exec($options = array())

		Deprecated: Just a compatibility stub to call execute().

	*/
	public function exec($options = array()) {
		execute($options);
	}
	/*
		get_data($template,$values)

		Extracts a associative $data array in the form based on a URL template and
		the $values extracted from the URL. For example, if the URL path was:

			'foos/1/bars/test/bazes/3'

		And the template was:

			'foos/{foo}/bars/{bar}/bazes/{baz}'

		The data extracted would look like this:

			$data= array(
				'foo' => 1,
				'bar' => 'test',
				'baz' => 3,
				'__template__' => 'foos/{foo}/bars/{bar}/bazes/{baz}',
			);

		NOTES:
			The index '__template__' index is used to store the URI Template for potential use
			by the function called by execute().

	*/
	private function get_data($template,$values) {
		$data = array();
		$variable_no = 1;
		foreach($template['variables'] as $variable) {
			$data[$variable] = $values[$variable_no++];
		}
		$data['__template__'] = $template;
		foreach($template['variables'] as $variable) {
			if (!$this->validate_url_variable_value($this->variables[$variable],$data[$variable],$data)) {
				header("HTTP/1.1 400 Bad Request");
				print 'ERROR: Invalid value [' . $value . '] for URL variable {' . $variable . "}\r\n" . $variable['description'];
				exit();
			}
		}
		return $data;
	}
	/*
		validate_url_variable_value($variable,$value) -> boolean (valid or not valid)

		Validates the URL variable based on it's validator:

			$validator ->
				One of:
					-- Function Name in global namespace (String)
					-- Array with Class name and Method Name (Array(String,String))
					-- Array with Object instance and Method Name (Array(Object,String))
					-- Array with Numeric or Date Range, inclusive (Array(Numeric,Numeric) or Array(Date,Date))

			$value ->
				Value to validate against

			NOTE: If parameters not passed correctly it generates a HTTP 500 status code and exits.

	*/
	private function validate_url_variable_value($variable,$value,$data) {
		$validator = $variable['validator'];
		if (is_string($validator)) {
			$is_valid = call_user_func($validator,$value,$data);
		} else if (is_array($validator)) {
			if (count($validator)==2) {
				if (method_exists($validator[0],$validator[1])) {
					$is_valid = call_user_func($validator,$value,$data);
				} else {
					// If not a object->method() or class::method() then a range inclusive
					$is_valid = ($validator[0]<=$value && $value<=$validator[1]);
				}
			}
		} else {
			header('HTTP/1.1 500 Internal Server Error');
			print "ERROR: Unexpected validator for value [$value]:\r\n\r\n";
			print_r($validator);
			exit();
		}
		return $is_valid;
	}
	/*
		get_path($template_name,$data) => url_path

		Get the actual URL path given a name for a template and the data to apply.

			$template_name =>
				The name of the URI Template to lookup in the list of templates.
				i.e. 'foos/{foo}/bars/{bar}/bazes/{baz}'

			$data =>
				The associate array of variable values to apply to the named URI Template, i.e.
					$data= array(
						'foo' => 1,
						'bar' => 'test',
						'baz' => 3,
					);

		Give the template and the data example above, the URL path would look like this:
				'foos/1/bars/test/bazes/3'

		NOTES:
			-- Generates a HTTP 500 status code if $template_name does not represent a define $template
	*/
	public function get_path($template_name,$data) {
		if (!isset($this->templates[$template_name])) {
			header('HTTP/1.1 500 Internal Server Error');
			print "ERROR!\r\n\r\nNo template [$template_name] defined.";
			exit();
		}
		$template = $this->templates[$template_name];
		$segments = $template['literals'];
		$i=1;
		foreach($template['variables'] as $variable) {
			array_splice($segments,$i,0,$data[$variable]);
			$i += 2;
		}
		$path = implode('',$segments);
		return $path;
	}

	/*
		show_no_match($debugging=false)

		Generate a HTTP 404 status code to be called when URL path does not match.

			$debugging => Optional [default=false]
				Specifies if debugging is turned on and thus should generate an error

		NOTES:
			-- Checks both the $debugging parameter and the $debugging class variable.
				 triggers 404 status code if either are true.
	*/
	private function show_no_match($debugging=false) {
		if (self::$debugging || $debugging) {
			header('HTTP/1.1 404 Not Found');
			header('Content-Type: text/plain');
			print "ERROR!\r\n\r\nNo URL was matched. Valid URLs match one of the following templates:\r\n";
			foreach($this->templates as $template) {
				print "   -- " . $template['template'] . "\r\n";
			}
			if (count($this->variables)) {
				print "\r\nThe following describe the templates variables from the above templates:\r\n";
				foreach($this->variables as $variable) {
					print "   -- " . $variable['name'] . ': ' . $variable['description'] . "\r\n";
				}
			}
			exit();
		}
	}
	/*
		get_version()

		Returns the version number of this class.

	*/
	public static function get_version() {
		return self::VERSION;
	}

}